/**************************************************************************************************
	$Header: /pub/cvsroot/mydns/src/mydns/axfr.c,v 1.22 2003/05/03 22:49:01 bboy Exp $

	Copyright (C) 2002, 2003  Don Moore <bboy@bboy.net>

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at Your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
**************************************************************************************************/

#include "named.h"

#define	AXFR_TIME_LIMIT		3600		/* AXFR may not take more than this long, overall */

static size_t total_records, total_octets;


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_ERROR
	Quits and outputs a warning message.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
/* Stupid compiler doesn't know exit from _exit... */
/* static void axfr_error(TASK *, const char *, ...) __attribute__ ((__noreturn__)); */
static void
axfr_error(TASK *t, const char *fmt, ...)
{
	va_list	ap;
	char		msg[BUFSIZ];

	va_start(ap, fmt);
	vsnprintf(msg, sizeof(msg), fmt, ap);
	va_end(ap);

	if (t)
		task_output_info(t);
	else
		Warnx("%s", msg);

#if HAVE_SHUTDOWN
	shutdown(t->fd, 2);
#endif
	close(t->fd);
	_exit(EXIT_FAILURE);
	/* NOTREACHED */
}
/*--- axfr_error() ------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_TIMEOUT
	Hard timeout called by SIGALRM after one hour.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_timeout(int dummy)
{
	axfr_error(NULL, _("AXFR timed out"));
}
/*--- axfr_timeout() ----------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_WRITE_WAIT
	Wait for the client to become ready to read.  Times out after `task_timeout' seconds.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_write_wait(TASK *t)
{
	fd_set	wfd;
	struct timeval tv;
	int		rv;

	FD_ZERO(&wfd);
	FD_SET(t->fd, &wfd);
	tv.tv_sec = task_timeout;
	tv.tv_usec = 0;
	if ((rv = select(t->fd + 1, NULL, &wfd, NULL, &tv)) < 0)
		axfr_error(t, "%s: %s", _("select"), strerror(errno));
	if (rv != 1 || !FD_ISSET(t->fd, &wfd))
		axfr_error(t, _("write timeout"));
}
/*--- axfr_write_wait() -------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_WRITE
	Writes the specified buffer, obeying task_timeout (via axfr_write_wait).
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_write(TASK *t, char *buf, size_t size)
{
	int rv, offset = 0;

	do
	{
		axfr_write_wait(t);
		if ((rv = write(t->fd, buf+offset, size-offset)) < 0)
			axfr_error(t, "write: %s", strerror(errno));
		if (!rv)
			axfr_error(t, _("client closed connection"));
		offset += rv;
	} while (offset < size);
}
/*--- axfr_write() ------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_REPLY
	Sends one reply to the client.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_reply(TASK *t)
{
	char len[2], *l = len;

	build_reply(t, 0);
	DNS_PUT16(l, t->replylen);
	axfr_write(t, len, SIZE16);
	axfr_write(t, t->reply, t->replylen);
	total_octets += SIZE16 + t->replylen;
	total_records++;

	/* Reset the pertinent parts of the task reply data */
	rrlist_free(&t->an);
	rrlist_free(&t->ns);
	rrlist_free(&t->ar);

	if (t->reply) free(t->reply);
	t->reply = NULL;
	t->replylen = 0;

	name_forget(t);

	if (t->rdata) free(t->rdata);
	t->rdata = NULL;
	t->rdlen = 0;

	/* Nuke question data */
	t->qdcount = 0;
	t->qdlen = 0;
}
/*--- axfr_reply() ------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	CHECK_XFER
	If the "xfer" column exists in the soa table, it should contain a list of wildcards separated
	by commas.  In order for this zone transfer to continue, one of the wildcards must match
	the client's IP address.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
check_xfer(TASK *t, MYDNS_SOA *soa)
{
	SQL_RES	*res = NULL;
	SQL_ROW	row;
	char		*ip = inet_ntoa(t->addr.sin_addr);
	char		query[512];
	size_t	querylen;
	int		ok = 0;

	if (!mydns_soa_use_xfer)
		return;

	querylen = snprintf(query, sizeof(query), "SELECT xfer FROM %s WHERE id=%u%s",
		mydns_soa_table_name, soa->id, mydns_rr_use_active ? " AND active=1" : "");

	if (!(res = sql_query(sql, query, querylen)))
		ErrSQL(sql, "%s: %s", desctask(t), _("error loading zone transfer access rules"));

	if ((row = sql_getrow(res)))
	{
		char *wild, *r;

		for (r = row[0]; !ok && (wild = strsep(&r, ",")); )
		{
			if (strchr(wild, '/'))
				ok = in_cidr(wild, t->addr.sin_addr);
			else if (wildcard_match(wild, ip))
				ok = 1;
		}
	}
	sql_free(res);

	if (!ok)
	{
		dnserror(t, DNS_R_REFUSED, ERR_NO_AXFR);
		axfr_reply(t);
		axfr_error(t, _("access denied"));
	}
}
/*--- check_xfer() ------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_GET_PTR_RECS
	Gets the PTR recs for the specified origin.  Returns a result set or NULL.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static SQL_RES *
axfr_get_ptr_recs(TASK *t, char *fields)
{
	uint8_t	ip[4];
	size_t	len = strlen(t->qname), querylen = 0;
	char		query[256];
	SQL_RES	*res = NULL;

	/* Is this "in-addr.arpa"? */
	if (len <= PTR_SUFFIX_LEN || strcmp(t->qname + (len - PTR_SUFFIX_LEN), PTR_SUFFIX))
		return (NULL);

	/* Split in-addr.arpa format address into quads */
	switch (mydns_extract_arpa(t->qname, ip))
	{
		case 3:
			querylen = snprintf(query, sizeof(query),
				"SELECT %s FROM %s WHERE "
				"ip >= %u AND ip <= %u%s",
				fields, mydns_ptr_table_name,
				INET_ATON(ip[2], ip[1], ip[0], 0),
				INET_ATON(ip[2], ip[1], ip[0], 255),
				mydns_ptr_use_active ? " AND active=1" : "");
			break;

		case 2:
			querylen = snprintf(query, sizeof(query),
				"SELECT %s FROM %s WHERE "
				"ip >= %u AND ip <= %u%s",
				fields, mydns_ptr_table_name,
				INET_ATON(ip[1], ip[0], 0, 0),
				INET_ATON(ip[1], ip[0], 255, 255),
				mydns_ptr_use_active ? " AND active=1" : "");
			break;

		case 1:
			querylen = snprintf(query, sizeof(query),
				"SELECT %s FROM %s WHERE "
				"ip >= %u AND ip <= %u%s",
				fields, mydns_ptr_table_name,
				INET_ATON(ip[0], 0, 0, 0),
				INET_ATON(ip[0], 255, 255, 255),
				mydns_ptr_use_active ? " AND active=1" : "");
			break;

		default:
			dnserror(t, DNS_R_REFUSED, ERR_NAME_FORMAT);
			axfr_reply(t);
			axfr_error(t, _("Invalid zone format"));
	}

	/* Get all PTR records for zone */
	if (querylen)
	{
		if (!(res = sql_query(sql, query, querylen)))
			ErrSQL(sql, "%s: %s", desctask(t), _("error loading ptr records"));

		/* If no results were found, refuse request */
		if (!sql_num_rows(res))
		{
			sql_free(res);
			return (NULL);
		}
		return (res);
	}
	return (NULL);
}
/*--- axfr_get_ptr_recs() -----------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_ZONE
	DNS-based zone transfer.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_zone(TASK *t, MYDNS_SOA *soa)
{
	SQL_RES	*res = NULL;
	SQL_ROW	row;
	char		query[256];
	size_t	querylen;

	/* Check optional "xfer" column and initialize reply */
	check_xfer(t, soa);
	reply_init(t);

	/* Send opening SOA record */
	rrlist_add(t, ANSWER, DNS_RRTYPE_SOA, (void *)soa, soa->origin);
	axfr_reply(t);

	/* Get all resource records for zone (if zone ID is nonzero, i.e. not manufactured) */
	if (soa->id)
	{
		querylen = snprintf(query, sizeof(query),
			"SELECT "MYDNS_RR_FIELDS" FROM %s WHERE zone=%u%s",
			mydns_rr_table_name, soa->id, mydns_rr_use_active ? " AND active=1" : "");
		if (!(res = sql_query(sql, query, querylen)))
			ErrSQL(sql, "%s: %s", desctask(t), _("error loading resource records"));

		/* For each RR, send one message */
		while ((row = sql_getrow(res)))
		{
			MYDNS_RR *rr;
			int len;

			if (!(rr = mydns_parse_rr(row, soa->origin)))
				continue;
			len = strlen(rr->name);
			if (rr->name[len-1] != '.')
			{
				if (*rr->name)
					strcat(rr->name, ".");
				strncat(rr->name, soa->origin, sizeof(rr->name) - len - 1);
			}

			/* Append origin to data for certain types */
			switch (rr->type)
			{
				case DNS_T_CNAME:
				case DNS_T_NS:
				case DNS_T_MX:
					len = strlen(rr->data);
					if (rr->data[len-1] != '.')
						strncat(rr->data, soa->origin, sizeof(rr->data) - len - 1);
					break;
				default:
					break;
			}
			rrlist_add(t, ANSWER, DNS_RRTYPE_RR, (void *)rr, rr->name);
			axfr_reply(t);
			mydns_rr_free(rr);
		}
		sql_free(res);
	}

	/* Send any PTR records */
	if ((res = axfr_get_ptr_recs(t, MYDNS_PTR_FIELDS)))
	{
		while ((row = sql_getrow(res)))
		{
			MYDNS_PTR	*ptr;
			uint8_t		q[4];
			char			fqdn[DNS_MAXNAMELEN+1];

			if (!(ptr = mydns_parse_ptr(row)))
				continue;

			memcpy(&q, &ptr->ip, sizeof(q));
			snprintf(fqdn, sizeof(fqdn), "%d.%d.%d.%d" PTR_SUFFIX, q[0], q[1], q[2], q[3]);
			rrlist_add(t, ANSWER, DNS_RRTYPE_PTR, (void *)ptr, fqdn);
			axfr_reply(t);
			mydns_ptr_free(ptr);
		}
		sql_free(res);
	}

	/* Send closing SOA record */
	rrlist_add(t, ANSWER, DNS_RRTYPE_SOA, (void *)soa, soa->origin);
	axfr_reply(t);

	mydns_soa_free(soa);
}
/*--- axfr_zone() -------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_GET_SOA
	Attempt to find a SOA record.  If SOA id is 0, we made it up.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static MYDNS_SOA *
axfr_get_soa(TASK *t)
{
	MYDNS_SOA *soa = NULL;

	/* Try to load SOA straight */
	if (mydns_soa_load(sql, &soa, t->qname) < 0)
		ErrSQL(sql, "%s: %s", desctask(t), _("error loading zone"));
	if (soa)
	{
#if DEBUG_ENABLED
		Debug("AXFR: %s: SOA record %u", soa->origin, soa->id);
#endif
		return (soa);
	}

	/* No SOA?  See if we have any PTR records */
	if (!mydns_soa_make(sql, &soa, t->qname, hostname) && soa)
	{
#if DEBUG_ENABLED
		Debug("AXFR: %s: SOA record fabricated", soa->origin);
#endif
		return (soa);
	}

	/* STILL no SOA?  We aren't authoritative */
	dnserror(t, DNS_R_REFUSED, ERR_ZONE_NOT_FOUND);
	axfr_reply(t);
	axfr_error(t, _("unknown zone"));
	/* NOTREACHED */
	return (NULL);
}
/*--- axfr_get_soa() ----------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR
	DNS-based zone transfer.  Send all resource records for in QNAME's zone to the client.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
void
axfr(TASK *t)
{
	struct timeval start, finish;								/* Time AXFR began and ended */
	MYDNS_SOA *soa;												/* SOA record for zone (may be bogus!) */

	/* Do generic startup stuff; this is a child process */
	signal(SIGALRM, axfr_timeout);
	alarm(AXFR_TIME_LIMIT);
	sql = NULL;
	db_connect();
	gettimeofday(&start, NULL);
	total_records = total_octets = 0;
	t->no_markers = 1;

	/* Get SOA for zone */
	soa = axfr_get_soa(t);

	/* Transfer that zone */
	axfr_zone(t, soa);

	/* Report result */
	gettimeofday(&finish, NULL);
#if DEBUG_ENABLED
	Debug("AXFR: %u records, %u octets, %.3fs", 
		total_records, total_octets,
		((finish.tv_sec + finish.tv_usec / 1000000.0) - (start.tv_sec + start.tv_usec / 1000000.0)));
#endif
	t->qdcount = 1;
	t->an.size = total_records;
	task_output_info(t);

#if HAVE_SHUTDOWN
	shutdown(t->fd, 2);
#endif
	close(t->fd);
	t->fd = -1;

	_exit(EXIT_SUCCESS);
}
/*--- axfr() ------------------------------------------------------------------------------------*/

/* vi:set ts=3: */
/* NEED_PO */
